<?php
namespace TYPO3\CMS\IndexedSearchMysql\Hook;

/*
 * This file is part of the TYPO3 CMS project.
 *
 * It is free software; you can redistribute it and/or modify it under
 * the terms of the GNU General Public License, either version 2
 * of the License, or any later version.
 *
 * For the full copyright and license information, please read the
 * LICENSE.txt file that was distributed with this source code.
 *
 * The TYPO3 project - inspiring people to share!
 */

/**
 * Class that hooks into Indexed Search and replaces standard SQL queries with MySQL fulltext index queries.
 *
 * @author Michael Stucki <michael@typo3.org>
 */
class MysqlFulltextIndexHook {

	/**
	 * @var \TYPO3\CMS\IndexedSearch\Controller\SearchFormController|\TYPO3\CMS\IndexedSearch\Domain\Repository\IndexSearchRepository
	 */
	public $pObj;

	const ANY_PART_OF_THE_WORD = '1';
	const LAST_PART_OF_THE_WORD = '2';
	const FIRST_PART_OF_THE_WORD = '3';
	const SOUNDS_LIKE = '10';
	const SENTENCE = '20';
	/**
	 * Gets a SQL result pointer to traverse for the search records.
	 *
	 * @param array $searchWordsArray Search words
	 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
	 * @return bool|\mysqli_result|object MySQLi result object / DBAL object
	 */
	public function getResultRows_SQLpointer($searchWordsArray, $freeIndexUid = -1) {
		// Build the search string, detect which fulltext index to use, and decide whether boolean search is needed or not
		$searchData = $this->getSearchString($searchWordsArray);
		// Perform SQL Search / collection of result rows array:
		$resource = FALSE;
		if ($searchData) {
			// Do the search:
			$GLOBALS['TT']->push('execFinalQuery');
			$resource = $this->execFinalQuery_fulltext($searchData, $freeIndexUid);
			$GLOBALS['TT']->pull();
		}
		return $resource;
	}

	/**
	 * Returns a search string for use with MySQL FULLTEXT query
	 *
	 * @param array $searchWordArray Search word array
	 * @return string Search string
	 */
	public function getSearchString($searchWordArray) {
		// Initialize variables:
		$count = 0;
		// Change this to TRUE to force BOOLEAN SEARCH MODE (useful if fulltext index is still empty)
		$searchBoolean = FALSE;
		$fulltextIndex = 'index_fulltext.fulltextdata';
		// This holds the result if the search is natural (doesn't contain any boolean operators)
		$naturalSearchString = '';
		// This holds the result if the search is boolen (contains +/-/| operators)
		$booleanSearchString = '';

		$searchType = (string)$this->pObj->getSearchType();

		// Traverse searchwords and prefix them with corresponding operator
		foreach ($searchWordArray as $searchWordData) {
			// Making the query for a single search word based on the search-type
			$searchWord = $searchWordData['sword'];
			$wildcard = '';
			if (strstr($searchWord, ' ')) {
				$searchType = self::SENTENCE;
			}
			switch ($searchType) {
				case self::ANY_PART_OF_THE_WORD:

				case self::LAST_PART_OF_THE_WORD:

				case self::FIRST_PART_OF_THE_WORD:
					// First part of word
					$wildcard = '*';
					// Part-of-word search requires boolean mode!
					$searchBoolean = TRUE;
					break;
				case self::SOUNDS_LIKE:
					$indexerObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\IndexedSearch\Indexer::class);
					// Initialize the indexer-class
					/** @var \TYPO3\CMS\IndexedSearch\Indexer $indexerObj */
					$searchWord = $indexerObj->metaphone($searchWord, $indexerObj->storeMetaphoneInfoAsWords);
					unset($indexerObj);
					$fulltextIndex = 'index_fulltext.metaphonedata';
					break;
				case self::SENTENCE:
					$searchBoolean = TRUE;
					// Remove existing quotes and fix misplaced quotes.
					$searchWord = trim(str_replace('"', ' ', $searchWord));
					break;
				}
				// Perform search for word:
				switch ($searchWordData['oper']) {
					case 'AND NOT':
						$booleanSearchString .= ' -' . $searchWord . $wildcard;
						$searchBoolean = TRUE;
						break;
					case 'OR':
						$booleanSearchString .= ' ' . $searchWord . $wildcard;
						$searchBoolean = TRUE;
						break;
					default:
						$booleanSearchString .= ' +' . $searchWord . $wildcard;
						$naturalSearchString .= ' ' . $searchWord;
			}
			$count++;
		}
		if ($searchType == self::SENTENCE) {
			$searchString = '"' . trim($naturalSearchString) . '"';
		} elseif ($searchBoolean) {
			$searchString = trim($booleanSearchString);
		} else {
			$searchString = trim($naturalSearchString);
		}
		return array(
			'searchBoolean' => $searchBoolean,
			'searchString' => $searchString,
			'fulltextIndex' => $fulltextIndex
		);
	}

	/**
	 * Execute final query, based on phash integer list. The main point is sorting the result in the right order.
	 *
	 * @param array $searchData Array with search string, boolean indicator, and fulltext index reference
	 * @param int $freeIndexUid Pointer to which indexing configuration you want to search in. -1 means no filtering. 0 means only regular indexed content.
	 * @return bool|\mysqli_result|object MySQLi result object / DBAL object
	 */
	protected function execFinalQuery_fulltext($searchData, $freeIndexUid = -1) {
		// Setting up methods of filtering results based on page types, access, etc.
		$pageJoin = '';
		// Indexing configuration clause:
		$freeIndexUidClause = $this->pObj->freeIndexUidWhere($freeIndexUid);
		// Calling hook for alternative creation of page ID list
		$searchRootPageIdList = $this->pObj->getSearchRootPageIdList();
		if ($hookObj = &$this->pObj->hookRequest('execFinalQuery_idList')) {
			$pageWhere = $hookObj->execFinalQuery_idList('');
		} elseif ($this->pObj->getJoinPagesForQuery()) {
			// Alternative to getting all page ids by ->getTreeList() where "excludeSubpages" is NOT respected.
			$pageJoin = ',
				pages';
			$pageWhere = 'pages.uid = ISEC.page_id
				' . $GLOBALS['TSFE']->cObj->enableFields('pages') . '
				AND pages.no_search=0
				AND pages.doktype<200
			';
		} elseif ($searchRootPageIdList[0] >= 0) {

			// Collecting all pages IDs in which to search;
			// filtering out ALL pages that are not accessible due to enableFields. Does NOT look for "no_search" field!
			$idList = array();
			foreach ($searchRootPageIdList as $rootId) {
				/** @var \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer $cObj */
				$cObj = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::class);
				$idList[] = $cObj->getTreeList( -1 * $rootId, 9999);
			}
			$pageWhere = ' ISEC.page_id IN (' . implode(',', $idList) . ')';
		} else {
			// Disable everything... (select all)
			$pageWhere = ' 1=1';
		}
		$searchBoolean = '';
		if ($searchData['searchBoolean']) {
			$searchBoolean = ' IN BOOLEAN MODE';
		}
		$resource = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
			'index_fulltext.*, ISEC.*, IP.*',
			'index_fulltext, index_section ISEC, index_phash IP' . $pageJoin,
			'MATCH (' . $searchData['fulltextIndex'] . ')
				AGAINST (' . $GLOBALS['TYPO3_DB']->fullQuoteStr($searchData['searchString'], 'index_fulltext') . $searchBoolean . ') ' .
				$this->pObj->mediaTypeWhere() . ' ' . $this->pObj->languageWhere() . $freeIndexUidClause . '
				AND index_fulltext.phash = IP.phash
				AND ISEC.phash = IP.phash
				AND ' . $pageWhere,
			'IP.phash,ISEC.phash,ISEC.phash_t3,ISEC.rl0,ISEC.rl1,ISEC.rl2,ISEC.page_id,ISEC.uniqid,IP.phash_grouping,IP.data_filename ,IP.data_page_id ,IP.data_page_reg1,IP.data_page_type,IP.data_page_mp,IP.gr_list,IP.item_type,IP.item_title,IP.item_description,IP.item_mtime,IP.tstamp,IP.item_size,IP.contentHash,IP.crdate,IP.parsetime,IP.sys_language_uid,IP.item_crdate,IP.cHashParams,IP.externalUrl,IP.recordUid,IP.freeIndexUid,IP.freeIndexSetId'
		);
		return $resource;
	}

}
